/**
 * 
 */
package com.mrroman.linksender.ioc;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Dependency inject module of link sender.
 * 
 * @author mrozekon
 * 
 */
public class ObjectStore {

	private static final Logger logger = Logger.getLogger(ObjectStore.class.getName());

	private Map<String, Object> objMap;

	private ResourceBundle messages;

    private static ObjectStore instance;

	private ObjectStore() {
		objMap = new HashMap<String, Object>();
	}

    public static synchronized ObjectStore getInstance() {
        if (instance == null)
            instance = new ObjectStore();

        return instance;
    }

    /**
     * Get object from object store. When class is annotated by @@Prototype,
     * method will create new object every time method would be invoked. Otherwise, only
     * one object is created. All dependencies will be injected to object.  
     * 
     * @param klass class of object
     * @return
     */
    public static <K> K getObject(Class<K> klass) {
    	return getObject(klass, false);
    }
    
    /**
     * Get object from object store. If forcePrototype is true, it will create new
     * object every time.   
     * 
     * @param klass class of object 
     * @param forcePrototype 
     * @return
     */
    public static <K> K getObject(Class<K> klass, boolean forcePrototype) {
		Map<String, Object> objMap = getInstance().objMap;
		Name name = klass.getAnnotation(Name.class);

		if (name == null)
			throw new IllegalStateException("Class " + klass.getName()
					+ " not annotated");

		Object obj = objMap.get(name.value());

		// check if object was already created
		if (obj != null)
			return klass.cast(obj);

		// if not, create instance
		try {
			obj = klass.newInstance();
			getInstance().resolveDependencies(klass, obj);
            if (!klass.isAnnotationPresent(Prototype.class) && !forcePrototype)
                objMap.put(name.value(), obj);

			// check if object has initialization method
			for (Method method : klass.getDeclaredMethods()) {
				if (method.isAnnotationPresent(Init.class))
					method.invoke(obj); // invoke method
			}

			return klass.cast(obj);
		} catch (InstantiationException e) {
			logger.log(Level.SEVERE, "Cannot instantiate class", e);
			return null;
		} catch (IllegalAccessException e) {
			logger.log(Level.SEVERE, "Cannot instantiate class", e);
			return null;
		} catch (InvocationTargetException e) {
			logger.log(Level.SEVERE, "Cannot instantiate class", e);
			return null;
		}

	}

	/**
	 * Resolves dependencies of class and injects them to object.
	 * 
	 * @param <K>
	 * @param klass
	 * @param obj
	 * @throws IllegalAccessException
	 */
	private <K> void resolveDependencies(Class<K> klass, Object obj)
			throws IllegalAccessException {

		// search all fields for @In annotation
		for (Field field : klass.getDeclaredFields()) {
			if (field.isAnnotationPresent(In.class)) {
				In inAnnotation = (In) field.getAnnotation(In.class);

/*				Object value = (inAnnotation.impl().equals(Object.class) ? getObject(field
						.getType())
						: objMap.get(inAnnotation.name()));*/
				
				Object value = null;
				
				if (inAnnotation.impl().equals(Object.class)) {
					// if @In annotation doesn't have impl set
					value = getObject(field.getType());
				} else {
					// Check if there is object already stored for 
					Name nameAnnotation = field.getType().getAnnotation(Name.class);
					
					if (nameAnnotation == null) { 
						throw new IllegalStateException(
								"Cannot inject object of class " + field.getType().getName() + " without @Name annotation");
					}
					
					if (objMap.containsKey(nameAnnotation.value()))
						value = objMap.get(nameAnnotation.value());
					else
						value = getObject(inAnnotation.impl());
				}

				if (value == null)
					logger.warning("Injecting null in " + klass.getName()
						+ "." + field.getName());

				field.setAccessible(true);
				field.set(obj, value);
			}

			resolveSpecialDependencies(klass, field, obj);
		}
	}

	private <K> void resolveSpecialDependencies(Class<K> klass, Field field, Object obj)
			throws IllegalArgumentException, IllegalAccessException {
	
		if (messages != null) {
			if (field.isAnnotationPresent(Locales.class)) {
				field.setAccessible(true);
				field.set(obj, messages);
			}
		}
		
		if (field.isAnnotationPresent(Log.class)) {
			field.setAccessible(true);
			field.set(obj, Logger.getLogger(klass.getName()));
		}
	}

	/**
	 * Add manually new object to pool. It can be used for testing, to inject mock objects.
	 * 
	 * @param klass
	 * @param value
	 */
	public static <K> void addObject(Class<K> klass, K value) {
		Name nameAnnotation = klass.getAnnotation(Name.class);
		
		if (nameAnnotation == null) {
			logger.severe("Class " + klass.getName() + " doesn't have @Name annotation.");
			return;
		}
		
		Map<String, Object> objMap = getInstance().objMap;
		
		if (objMap.containsKey(nameAnnotation.value()))
			logger.warning("Object " + nameAnnotation.value() + "  already added.");

		objMap.put(nameAnnotation.value(), value);
	}

	public void setMessageBundle(ResourceBundle resourceBundle) {
		this.messages = resourceBundle;
	}
	
}
